import Mustache, Highlights
import .WeaveMarkdown
using Dates
using Markdown
using REPL.REPLCompletions: latex_symbols

function format(doc::WeaveDoc)
    formatted = AbstractString[]
    docformat = doc.format

    # Complete format dictionaries with defaults
    formatdict = docformat.formatdict
    get!(formatdict, :termstart, formatdict[:codestart])
    get!(formatdict, :termend, formatdict[:codeend])
    get!(formatdict, :out_width, nothing)
    get!(formatdict, :out_height, nothing)
    get!(formatdict, :fig_pos, nothing)
    get!(formatdict, :fig_env, nothing)

    docformat.formatdict[:cwd] = doc.cwd # pass wd to figure formatters
    docformat.formatdict[:theme] = doc.highlight_theme

    strip_header!(doc)

    for chunk in copy(doc.chunks)
        result = format_chunk(chunk, formatdict, docformat)
        push!(formatted, result)
    end

    formatted = join(formatted, "\n")
    # Render using a template if needed
    rendered = render_doc(formatted, doc, doc.format)

    return rendered
end

"""
  render_doc(formatted::AbstractString, format)

Render formatted document to a template
"""
function render_doc(formatted, doc::WeaveDoc, format)
    return formatted
end

function highlight(
    mime::MIME,
    source::AbstractString,
    lexer,
    theme = Highlights.Themes.DefaultTheme,
)
    return sprint((io, x) -> Highlights.highlight(io, mime, x, lexer, theme), source)
end

function stylesheet(m::MIME, theme)
    return sprint((io, x) -> Highlights.stylesheet(io, m, x), theme)
end

function render_doc(formatted, doc::WeaveDoc, format::JMarkdown2HTML)
    css = stylesheet(MIME("text/html"), doc.highlight_theme)
    path, wsource = splitdir(abspath(doc.source))
    # wversion = string(Pkg.installed("Weave"))
    wversion = ""
    wtime = string(Date(now()))

    if isempty(doc.css)
        theme_css =
            read(joinpath(dirname(@__FILE__), "../templates/skeleton_css.css"), String)
    else
        theme_css = read(doc.css, String)
    end

    if isa(doc.template, Mustache.MustacheTokens)
        template = doc.template
    elseif isempty(doc.template)
        template = Mustache.template_from_file(joinpath(
            dirname(@__FILE__),
            "../templates/julia_html.tpl",
        ))
    else
        template = Mustache.template_from_file(doc.template)
    end

    return Mustache.render(
        template;
        themecss = theme_css,
        highlightcss = css,
        body = formatted,
        header_script = doc.header_script,
        source = wsource,
        wtime = wtime,
        wversion = wversion,
        [Pair(Symbol(k), v) for (k, v) in doc.header]...,
    )
end

function render_doc(formatted, doc::WeaveDoc, format::JMarkdown2tex)
    highlight = stylesheet(MIME("text/latex"), doc.highlight_theme)
    path, wsource = splitdir(abspath(doc.source))
    # wversion = string(Pkg.installed("Weave"))
    wversion = ""
    wtime = string(Date(now()))

    if isa(doc.template, Mustache.MustacheTokens)
        template = doc.template
    elseif isempty(doc.template)
        template = Mustache.template_from_file(joinpath(
            dirname(@__FILE__),
            "../templates/julia_tex.tpl",
        ))
    else
        template = Mustache.template_from_file(doc.template)
    end

    return Mustache.render(
        template;
        body = formatted,
        highlight = highlight,
        [Pair(Symbol(k), v) for (k, v) in doc.header]...,
    )
end

strip_header!(doc::WeaveDoc) = strip_header!(doc.chunks[1], doc.doctype)
function strip_header!(docchunk::DocChunk, doctype)
    doctype == "pandoc" && return
    content = docchunk.content[1].content
    if (m = match(HEADER_REGEX, content)) !== nothing
        # TODO: is there other format where we want to keep headers ?
        docchunk.content[1].content = if doctype != "github"
            lstrip(replace(content, HEADER_REGEX => ""))
        else
            # only strips Weave headers
            header = YAML.load(m[:header])
            delete!(header, WEAVE_OPTION_NAME)
            if isempty(header)
                lstrip(replace(content, HEADER_REGEX => ""))
            else
                lstrip(replace(content, HEADER_REGEX => "---\n$(YAML.write(header))---"))
            end
        end
    end
end
strip_header!(codechunk::CodeChunk, doctype) = return

function format_chunk(chunk::DocChunk, formatdict, docformat)
    return join([format_inline(c) for c in chunk.content], "")
end

function format_inline(inline::InlineText)
    return inline.content
end

function format_inline(inline::InlineCode)
    isempty(inline.rich_output) || return inline.rich_output
    isempty(inline.figures) || return inline.figures[end]
    isempty(inline.output) || return inline.output
end

function ioformat!(io::IOBuffer, out::IOBuffer, fun = WeaveMarkdown.latex)
    text = String(take!(io))
    if !isempty(text)
        m = Markdown.parse(text, flavor = WeaveMarkdown.weavemd)
        write(out, string(fun(m)))
    end
end

function addspace(op, inline)
    inline.ctype == :line && (op = "\n$op\n")
    return op
end

function format_chunk(chunk::DocChunk, formatdict, docformat::JMarkdown2tex)
    out = IOBuffer()
    io = IOBuffer()
    for inline in chunk.content
        if isa(inline, InlineText)
            write(io, inline.content)
        elseif !isempty(inline.rich_output)
            ioformat!(io, out)
            write(out, addspace(inline.rich_output, inline))
        elseif !isempty(inline.figures)
            write(io, inline.figures[end], inline)
        elseif !isempty(inline.output)
            write(io, addspace(inline.output, inline))
        end
    end
    ioformat!(io, out)
    formatdict[:keep_unicode] || return uc2tex(String(take!(out)))
    return String(take!(out))
end

function format_chunk(chunk::DocChunk, formatdict, docformat::JMarkdown2HTML)
    out = IOBuffer()
    io = IOBuffer()
    fun = WeaveMarkdown.html
    for inline in chunk.content
        if isa(inline, InlineText)
            write(io, inline.content)
        elseif !isempty(inline.rich_output)
            ioformat!(io, out, fun)
            write(out, addspace(inline.rich_output, inline))
        elseif !isempty(inline.figures)
            write(io, inline.figures[end])
        elseif !isempty(inline.output)
            write(io, addspace(inline.output, inline))
        end
    end
    ioformat!(io, out, fun)
    return String(take!(out))
end

function format_chunk(chunk::CodeChunk, formatdict, docformat)
    # Fill undefined options with format specific defaults
    chunk.options[:out_width] == nothing &&
        (chunk.options[:out_width] = formatdict[:out_width])
    chunk.options[:fig_pos] == nothing && (chunk.options[:fig_pos] = formatdict[:fig_pos])

    # Only use floats if chunk has caption or sets fig_env
    if chunk.options[:fig_cap] != nothing && chunk.options[:fig_env] == nothing
        (chunk.options[:fig_env] = formatdict[:fig_env])
    end

    if haskey(formatdict, :indent)
        chunk.content = indent(chunk.content, formatdict[:indent])
    end

    chunk.content = format_code(chunk.content, docformat)

    if !chunk.options[:eval]
        if chunk.options[:echo]
            result = "$(formatdict[:codestart])\n$(chunk.content)$(formatdict[:codeend])"
            return result
        else
            r = ""
            return r
        end
    end

    if chunk.options[:term]
        result = format_termchunk(chunk, formatdict, docformat)
    else

        if chunk.options[:echo]
            # Convert to output format and highlight (html, tex...) if needed
            result = "$(formatdict[:codestart])$(chunk.content)$(formatdict[:codeend])\n"
        else
            result = ""
        end

        if (strip(chunk.output) != "" || strip(chunk.rich_output) != "") &&
           (chunk.options[:results] != "hidden")
            if chunk.options[:results] != "markup" && chunk.options[:results] != "hold"
                strip(chunk.output) ≠ "" && (result *= "$(chunk.output)\n")
                strip(chunk.rich_output) ≠ "" && (result *= "$(chunk.rich_output)\n")
            else
                if chunk.options[:wrap]
                    chunk.output =
                        "\n" * wraplines(chunk.output, chunk.options[:line_width])
                    chunk.output = format_output(chunk.output, docformat)
                else
                    chunk.output = "\n" * rstrip(chunk.output)
                    chunk.output = format_output(chunk.output, docformat)
                end

                if haskey(formatdict, :indent)
                    chunk.output = indent(chunk.output, formatdict[:indent])
                end
                strip(chunk.output) ≠ "" && (
                    result *= "$(formatdict[:outputstart])$(chunk.output)\n$(formatdict[:outputend])\n"
                )
                strip(chunk.rich_output) ≠ "" && (result *= chunk.rich_output * "\n")
            end
        end

    end

    # Handle figures
    if chunk.options[:fig] && length(chunk.figures) > 0
        if chunk.options[:include]
            result *= formatfigures(chunk, docformat)
        end
    end

    return result
end

function format_output(result::AbstractString, docformat)
    return result
end

function format_output(result::AbstractString, docformat::JMarkdown2HTML)
    return Markdown.htmlesc(result)
end

function format_output(result::AbstractString, docformat::JMarkdown2tex)
    # Highligts has some extra escaping defined, eg of $, ", ...
    result_escaped = sprint(
        (io, x) ->
            Highlights.Format.escape(io, MIME("text/latex"), x, charescape = true),
        result,
    )
    docformat.formatdict[:keep_unicode] || return uc2tex(result_escaped, true)
    return result_escaped
end

function format_code(result::AbstractString, docformat)
    return result
end

function format_code(result::AbstractString, docformat::JMarkdown2tex)
    highlighted = highlight(
        MIME("text/latex"),
        strip(result),
        Highlights.Lexers.JuliaLexer,
        docformat.formatdict[:theme],
    )
    docformat.formatdict[:keep_unicode] || return uc2tex(highlighted)
    return highlighted
    # return "\\begin{minted}[mathescape, fontsize=\\small, xleftmargin=0.5em]{julia}\n$result\n\\end{minted}\n"
end

# Convert unicode to tex, escape listings if needed
function uc2tex(s, escape = false)
    for key in keys(latex_symbols)
        if escape
            s = replace(s, latex_symbols[key] => "(*@\\ensuremath{$(texify(key))}@*)")
        else
            s = replace(s, latex_symbols[key] => "\\ensuremath{$(texify(key))}")
        end
    end
    return s
end

# Make julia symbols (\bf* etc.) valid latex
function texify(s)
    ts = ""
    if occursin(r"^\\bf[A-Z]$", s)
        ts = replace(s, "\\bf" => "\\bm{\\mathrm{") * "}}"
    elseif startswith(s, "\\bfrak")
        ts = replace(s, "\\bfrak" => "\\bm{\\mathfrak{") * "}}"
    elseif startswith(s, "\\bf")
        ts = replace(s, "\\bf" => "\\bm{\\") * "}"
    elseif startswith(s, "\\frak")
        ts = replace(s, "\\frak" => "\\mathfrak{") * "}"
    else
        ts = s
    end
    return ts
end

function format_code(result::AbstractString, docformat::JMarkdown2HTML)
    return highlight(
        MIME("text/html"),
        strip(result),
        Highlights.Lexers.JuliaLexer,
        docformat.formatdict[:theme],
    )
end

function format_code(result::AbstractString, docformat::Pandoc2HTML)
    return highlight(
        MIME("text/html"),
        strip(result),
        Highlights.Lexers.JuliaLexer,
        docformat.formatdict[:theme],
    )
end

function format_termchunk(chunk, formatdict, docformat)
    if chunk.options[:echo] && chunk.options[:results] != "hidden"
        result = "$(formatdict[:termstart])$(chunk.output)\n" * "$(formatdict[:termend])\n"
    else
        result = ""
    end
    return result
end

function format_termchunk(chunk, formatdict, docformat::JMarkdown2HTML)
    if chunk.options[:echo] && chunk.options[:results] != "hidden"
        result = highlight(
            MIME("text/html"),
            strip(chunk.output),
            Highlights.Lexers.JuliaConsoleLexer,
            docformat.formatdict[:theme],
        )
    else
        result = ""
    end
    return result
end

function format_termchunk(chunk, formatdict, docformat::Pandoc2HTML)
    if chunk.options[:echo] && chunk.options[:results] != "hidden"
        result = highlight(
            MIME("text/html"),
            strip(chunk.output),
            Highlights.Lexers.JuliaConsoleLexer,
            docformat.formatdict[:theme],
        )
    else
        result = ""
    end
    return result
end

function format_termchunk(chunk, formatdict, docformat::JMarkdown2tex)
    if chunk.options[:echo] && chunk.options[:results] != "hidden"
        result = highlight(
            MIME("text/latex"),
            strip(chunk.output),
            Highlights.Lexers.JuliaConsoleLexer,
            docformat.formatdict[:theme],
        )
        # return "\\begin{minted}[mathescape, fontsize=\\small, xleftmargin=0.5em]{julia}\n$result\n\\end{minted}\n"
    else
        result = ""
    end
    return result
end

function indent(text, nindent)
    return join(map(x -> string(repeat(" ", nindent), x), split(text, "\n")), "\n")
end

function wraplines(text, line_width = 75)
    result = AbstractString[]
    lines = split(text, "\n")
    for line in lines
        if length(line) > line_width
            push!(result, wrapline(line, line_width))
        else
            push!(result, line)
        end
    end

    return strip(join(result, "\n"))
end

function wrapline(text, line_width = 75)
    result = ""
    while length(text) > line_width
        result *= first(text, line_width) * "\n"
        text = chop(text, head = line_width, tail = 0)
    end
    result *= text
end
